package org.dromara.common.doc.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.doc.config.properties.SpringDocProperties;
import org.dromara.common.doc.handler.OpenApiHandler;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.service.SecurityService;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：Swagger 文档配置
 */
@RequiredArgsConstructor
@AutoConfiguration(before = SpringDocConfiguration.class)
// 启用SpringDocProperties配置属性
@EnableConfigurationProperties(SpringDocProperties.class)
// 当配置项springdoc.api-docs.enabled设为true时启用，若未设置则默认启用
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
public class SpringDocConfig {

    /**
     * 服务器属性对象，封装了与服务器相关的各种配置和属性，
     * 如服务器地址、端口号等。使用final修饰确保该对象在初始化后不可变，
     * 有助于提高程序的可靠性和线程安全性。
     */
    private final ServerProperties serverProperties;

    @Bean
    @ConditionalOnMissingBean(OpenAPI.class)
    public OpenAPI openApi(SpringDocProperties properties) {
        OpenAPI openApi = new OpenAPI();
        // 设置文档基本信息，如标题、描述、版本等
        SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
        Info info = convertInfo(infoProperties);
        openApi.info(info);
        // 设置扩展文档信息，如外部文档、标签、路径和组件等
        openApi.externalDocs(properties.getExternalDocs());
        openApi.tags(properties.getTags());
        openApi.paths(properties.getPaths());
        openApi.components(properties.getComponents());

        // 设置安全要求，根据配置的SecuritySchemes生成SecurityRequirement对象
        Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
        List<SecurityRequirement> list = new ArrayList<>();
        SecurityRequirement securityRequirement = new SecurityRequirement();
        keySet.forEach(securityRequirement::addList);
        list.add(securityRequirement);
        openApi.security(list);

        return openApi;
    }

    /**
     * 将SpringDoc的InfoProperties对象转换为Swagger的Info对象
     *
     * @param infoProperties SpringDoc的InfoProperties对象，包含标题、描述、联系人、许可证和版本信息
     * @return 返回转换后的Swagger Info对象
     */
    private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
        Info info = new Info();
        info.setTitle(infoProperties.getTitle());
        info.setDescription(infoProperties.getDescription());
        info.setContact(infoProperties.getContact());
        info.setLicense(infoProperties.getLicense());
        info.setVersion(infoProperties.getVersion());
        return info;
    }

    /**
     * 自定义 openapi 处理器
     * 通过OpenAPI构建器创建OpenAPIService实例
     *
     * 此方法使用各种配置和服务来构建OpenAPIService，以实现对OpenAPI文档的生成和管理
     * 它依赖于OpenAPI定义、安全服务配置、SpringDoc配置属性、属性解析工具、
     * OpenAPI构建器自定义器、服务器基础URL自定义器和Javadoc提供者
     *
     * @param openAPI OpenAPI定义的可选对象，用于描述API的元数据
     * @param securityParser 安全服务，用于解析和验证API的安全性相关配置
     * @param springDocConfigProperties SpringDoc的配置属性，用于定制OpenAPI文档生成
     * @param propertyResolverUtils 属性解析工具，用于从不同来源解析和合并属性
     * @param openApiBuilderCustomisers OpenAPI构建器自定义器的可选列表，用于自定义OpenAPI文档生成
     * @param serverBaseUrlCustomisers 服务器基础URL自定义器的可选列表，用于自定义OpenAPI文档中的服务器URL
     * @param javadocProvider Javadoc提供者的可选实例，用于将Javadoc集成到OpenAPI文档中
     * @return 返回一个OpenAPIService实例，用于处理和生成OpenAPI文档
     */
    @Bean
    public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
                                         SecurityService securityParser,
                                         SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
                                         Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
                                         Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
        return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
    }

    /**
     * 对已经生成好的 OpenApi 进行自定义操作
     *
     * 配置OpenApiCustomizer Bean，用于自定义OpenAPI文档的生成
     * 主要功能是根据服务器的上下文路径调整OpenAPI文档中的路径
     * 当上下文路径为空或根路径时，不添加任何前缀，否则使用上下文路径作为前缀
     */
    @Bean
    public OpenApiCustomizer openApiCustomizer() {
        // 获取服务器的上下文路径
        String contextPath = serverProperties.getServlet().getContextPath();
        // 初始化最终的上下文路径，空或根路径时不添加前缀
        String finalContextPath;
        if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
            finalContextPath = "";
        } else {
            finalContextPath = contextPath;
        }
        // 对所有路径增加前置上下文路径
        return openApi -> {
            // 获取原有的路径对象
            Paths oldPaths = openApi.getPaths();
            // 如果已经是自定义的路径对象，则无需重复转换
            if (oldPaths instanceof PlusPaths) {
                return;
            }
            // 创建自定义的路径对象，用于支持路径前缀添加
            PlusPaths newPaths = new PlusPaths();
            // 遍历原有路径，添加到新的路径对象中，并加上前缀
            oldPaths.forEach((k, v) -> newPaths.addPathItem(finalContextPath + k, v));
            // 更新OpenAPI对象中的路径为新的路径对象
            openApi.setPaths(newPaths);
        };
    }

    /**
     * 单独使用一个类便于判断 解决springdoc路径拼接重复问题
     *
     * PlusPaths 类继承自 Paths 类，用于提供额外的路径处理功能
     * 该类是一个静态内部类，可以在不依赖外部实例的情况下使用
     */
    static class PlusPaths extends Paths {

        /**
         * PlusPaths 类的构造方法
         * 它调用父类的构造方法进行初始化
         */
        public PlusPaths() {
            super();
        }
    }

}
